前端单元测试-覆盖率和增量覆盖率

2022.1.19 星期三 :

# 覆盖率报告
我在jest环境中工作,在寻找test coverage时,它显示在一行中,用符号E,这是什么?
我发现,E代表else path not taken,这意味着对于标记的if/else语句,if路径已经过测试,而不是else。

报告结果分析

通过 Istanbul 得到的测试覆盖率报告

四个测量维度

行覆盖率(line coverage):每个可执行代码行是否都执行了?
函数覆盖率(function coverage):每个函数是否都调用了?
分支覆盖率(branch coverage):每个流程控制的各个分支是否都执行了?
语句覆盖率(statement coverage):每个语句是否都执行了?

行(Lines of Source Code) vs 可执行代码行(Lines of Executable Code)

“行覆盖率”中的行是指可执行代码行(Lines of Executable Code),而不是源文件中所有的行(含空行)——(Lines of Source Code)。

可执行代码行:
一般来说,包含语句的每一行都应被视为可执行行。而复合语句(简称为语句块,用 {} 括起来)会被忽略(但其内容除外)。

注:对于可执行行的定义,不同覆盖率引擎可能会存在一些差异。

具体以下东西会被忽略(即视为非可执行行,+0):

  • 非语句
    一些覆盖率引擎会将以下两点视为可执行行,而 Istanbul 会忽略它们:
    • 该行只包含标点符号:}、});、;
    • 定义时的方法(函数)名
  • import、声明
    import、声明都被视为非可执行行(+0),require、赋值等语句视为可执行行(+1)
    如果某行存在可执行代码,则这一整行会被视为可执行代码行。
    而如果一个语句被拆分为多行,则该可执行代码块中,仅第一行被会视为可执行行。 绿色方框的是 Lines of Source Code、红色红框内与底色不同的色块是 Lines of Executable Code

    可执行代码行 vs 语句

    一般情况下,如果我们遵守良好的代码规范,可执行代码行和语句的表现是一致的。然而当我们将两个语句放一行时,就会得到不同的结果。

    流程控制

    其他标识

    测试覆盖率报告出现的标识有:
    ‘E’:’else path not taken’,表示 if/else 语句的 if(含 else if)分支已测试,而 else 分支未测试。
    ‘I’:’if path not taken’,与上面的 ‘E’ 相反,即 if(含 else if) 分支未测试。
    ‘Nx’:表示当前可执行代码行被执行的总次数。
    粉色(背景色):语句/函数未覆盖。
    黄色(背景色):分支未覆盖。

    html 报告效果:行覆盖(绿色),未覆盖(红色),半覆盖(黄色),无视(白色)

    # 工具/手段

    chrome Dev Tools

    chrome 浏览器的 DevTools 给我们提供了度量页面代码(JS、CSS)覆盖率的工具 Coverage。
    使用方式:Dev tools —— More tools —— Coverage

    • 可度量代码类型:JS CSS
    • 统计可视化形式:
      1. 使用率是以byte字节来计算的;
      2. 当我们选择一段脚本资源即可在 Source 栏可以看到加载页面时当前资源 run过得代码(蓝色)和没有run过得代码(红色);

    缺点:显然,目前大部分网页上的JS脚本基本都是经过混淆压缩打包过后的产物,对于开发者而言,这种覆盖率可读性及参考价值不大。
    TIPS:当然,如果在拥有 source map 的情况下也是可以用浏览器查看源代码的覆盖率的:

    1. 在 source tab 中找到当前页面的 js 资源文件(当然已经被混淆的面目全非)
    2. 输入 sourcemap URL(以 def 发布平台为例,在构建结果中可找到)
    3. 在 webpack:// 目录下即可查看对应源码的大致覆盖率(不过没有什么消费价值)

    Istanbul(NYC)

    Istanbul(NYC)这个软件以土耳其最大城市伊斯坦布尔命名,因为土耳其地毯世界闻名,而地毯则是用来覆盖的。
    Istanbul或者 NYC(New York City,基于 istanbul 实现) 是度量 JavaScript 程序的代码覆盖率工具,目前绝大多数的node代码测试框架使用该工具来获得测试报告,其有四个测量维度:

    • line coverage(行覆盖率-每一行是否都执行了) 【一般我们关注这个信息】
    • function coverage(函数覆盖率-每个函数是否都调用了)
    • branch coverage(分支覆盖率-是否每个 if 代码块都执行了)
    • statement coverage(语句覆盖率-是否每个语句都执行了)
      可以度量的代码类型:JS TS
      统计可视化的形式:HTMLterminal

    缺点:目前使用 istanbul 度量网页前端JS代码覆盖率没有非侵入的方案,采用的是在编译构建时修改构建结果的方式埋入统计代码,再在运行时进行统计展示。

    我们可以使用 babel-plugin-istanbul 插件在对源代码在 AST 级别进行包装重写,这种编译方式也叫 代码插桩 / 插桩构建(instrument)
    <!–

    插桩构建

    istabul 确实也是这么做的,babel-plugin-istanbul 在构建过程中分析 AST 并将相应统计单元(语句、函数、分支等)做装饰代码的添加,最终在代码运行之后,输出一份 json 格式的数据:
    当我们在运行代码过后,得到了上面的 json 便可以消费它了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    {

    "/Users/xxx/test/istanbul.js":{
    "path":"/Users/bairuobing/test/istanbul.js",
    "s":{
    "1":1,
    "2":0,
    "3":1
    },
    "b":{

    },
    "f":{
    "1":0
    },

    "fnMap":{ // function 的开始结束位置信息
    "1":{
    "name":"add",
    },
    }
    }
    }
    ```
    -->
    <!--
    ## 线程池概述代码覆盖率在iHome Rax开发套件 Tbox 中的应用
    tips:tbox 每平每屋 消费者端 本地开发套件 既然我们知道了源代码的代码覆盖率,我们可以用它为性能优化做些什么贡献呢?
    当工程主 bundle 较大,那么采用拆包较大的/无用的前端组件来瘦身首屏主 JS 包不失为一种可行的选择,此时就可以根据代码覆盖率来决定优化哪些代码。
    ### 代码分割
    React.lazy 已经为我们提供了一种不错的思路,就是利用动态加载模块规范 import() (webpack对import()解析为代码分割)的能力来实现前端组件代码懒加载/动态加载。

    以此为灵感,那么为何不将某些组件通过动态引入的方式加载,来以此换取首页 bundle 的瘦身呢?
    ### 下一步
    我们能通过代码覆盖率统计出哪些组件的代码首屏使用率为0(或者门槛值30%以下),并在项目工程中自动生成一个持久化的文件配置(app.json中),之后依据配置将这些低使用率的组件代码在生产构建时将产物代码改写为动态引入。

    于是有了以下方案:


    ## 写在最后
    istanbul 在 node 环境下跑测试用例代码能度量覆盖率是由于其对运行时模块加载器的源代码拦截,但是比较遗憾的是,本文介绍的代码插桩分析覆盖率这会引入一些多余的桩代码,或许采用 puppeteer 无头浏览器提供的Coverage api + sourceMap 逆编译的思路来进行度量是一种更加完美的方式,期待与诸君一起探索,继续努力!

    作者:阿里巴巴大淘宝技术
    链接:https://www.zhihu.com/question/22244568/answer/2516883782
    -->

    <!-- END: #33 -->
    <!-- 4. 条件加载被异步化的组件。 #41内容 -->
    \# 意义
    <!-- \# 41 [什么是代码覆盖率?](https://www.zhihu.com/question/22244568/answer/1284218505) -->
    ## 代码覆盖率的意义
    1. 分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
    2. 检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
    3. 代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。
    \## 最后
    覆盖率数据只能代表你测试过哪些代码,不能代表你是否测试好这些代码。不要过于相信覆盖率数据。不要只拿语句覆盖率(行覆盖率)来考核你的测试人员。测试人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的案例,哪怕多设计出来的案例对覆盖率一点影响也没有。
    路径覆盖率 > 判定覆盖 > 语句覆盖
    <!-- END: 41 -->

    <!-- \#222 [你需要知道的覆盖率 / 原理 / 解惑 / 增量覆盖率计算](https://testerhome.com/topics/15866) -->
    ## 高覆盖率
    Q: 精细的覆盖率统计真的比粗粒度的覆盖率统计好么?
    A: 未必,越精细意味着:1. 统计耗时越大 2. 牵涉到高覆盖率到底有什么意义(见下)
    ### 高覆盖率真的很有用么?
    <!-- 这是一个经典疑问(误解),看代码 -->
    ```js
    if (x > 0)
    y = 5 / x; // 没有除零错误
    else
    y = 5 / (x + 1); // x==-1除零错误

    显然两个 x 输入(测试用例)即可完全覆盖,x==2 和 x==-2,但是对找到 x==-1 除零错误没有卵用,以小见大,结论是:

    • 高覆盖的测试用例 != 测试用例有用
    • 没覆盖的分支 == 该分支上的任何错误肯定都测不到,注意错误不限于 Exception

    # 覆盖率原理

    原理介绍

    覆盖率检测是用来判断单测完整性的,jest 和 karma 都提供了这种功能:
    jest 和 karama 都是基于 istanbul 做的覆盖率检测,我们来探究下 istanbul 的实现原理。

    npx istanbul instrument ./test.js -o ./out.js
    instrument 是指函数插桩,也就是透明的给函数添加一些代码。
    这就是转换后的代码,在每一个 statement,每一个 function、每一个 branch 都做了计数,分别是 s、f、b 属性。

    函数插桩是基于 AST,找到 statement、function、branch 的 AST,在前面插入插桩代码的 AST。

    istanbul 的源码: 通过 esprima(js parser)来把代码 parse 成 AST,然后对 AST 进行插桩。
    插桩代码分为两部分,一部分是初始化全局对象的代码,一部分是每个分支、语句、函数的计数代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo() {
    console.log('test')
    }
    // 转换后
    function foo() {
    AAAA.f['1']++;
    AAAA.s['2']++;
    console.log('test')
    }

    ### istanbul 是怎么做到透明的插桩的呢?
    看过之前一篇 require hook 的魔术那篇文章的小伙伴知道,nodejs 的模块加载是分为 load、extension[‘.js’]、compile 这几步的。

    我们只需要重写 extension[‘.js’] 这一步,就能做到透明的代码转换。

    总结
    jest 和 karma 都基于 istanbul 实现了覆盖率检测。覆盖率统计的原理就是函数插桩,基于 AST 在代码的 statement、function、branch 处插入计数代码,同时通过 require hook 实现了透明的转换。这样代码一执行就能拿到统计数据,自然就可以算出覆盖率了。

    coverage命令

    istanbul-lib-coverage:提供覆盖率信息的只读视图,能够合并和汇总覆盖率信息的api
    istanbul-lib-report:istanbul库生成报告的核心程序
    istanbul-reports:大体提供报告输出的公共能力api
    这三个库使用围绕着istanbul,因此该库基本大概率提供了核心的覆盖率能力,下面去验证下。

    Istanbul instruments your ES5 and ES2015+ JavaScript code with line counters, so that you can track how well your unit-tests exercise your codebase.
    istanbul,为ES5和ES2015+JavaScript代码插入行计数器,这样您就可以跟踪单元测试对代码库的运行情况。
    <!– npm install -g istanbul
    安装完成后,执行下istanbul help查看下它所支持的命名,所有核心命令如下:

    check-coverage:根据JSON文件的覆盖率阈值检查总体/每个文件的覆盖率。如果不满足阈值,则退出1,否则退出0。
    cover:透明地将覆盖率信息添加到节点命令。在执行结束时保存coverage.json和报告。
    instrument:插入文件或目录树,并将插入指令的代码写入所需的输出位置。
    report:为上一次运行中生成的JSON对象写入覆盖率报告。

    instrument命令

    istanbul instrument ./test.js -o ./test-inst.js

    1. 它创建了一个全局对象用于存储统计信息
    2. 它往函数执行代码各个对应位置插入了统计代码,对应代码块执行就+1
    3. 命名及其复杂防止与全局命名冲突。 这便是整个istanbul的流程,也是生成覆盖率的基本原理。
      –>

    Istanbul或nyc

    nyc: https://github.com/istanbuljs/nyc
    How Istanbul works

    Istanbul instruments your ES5 and ES2015+ JavaScript code with line counters, so that you can track how well your unit-tests exercise your codebase.

    The nyc command-line-client for Istanbul works well with most JavaScript testing frameworks: tap, mocha, AVA, etc.

    Note: jest or tap, you do not need to install nyc. Those runners already have the IstanbulJS libraries to provide coverage for you. Follow their documentation to enable and configure coverage reporting.

    Using Istanbul With Mocha

    Integrating with Coveralls
    coveralls.io is a great tool for adding coverage reporting to your continuous-integration flow. Here’s how to get Istanbul integrated with coveralls and travis-ci.org:

    # 增量覆盖率
    <!–
    # 70 前端代码覆盖率增量计算
    ## 前端代码覆盖率增量覆盖的困难
    针对前端代码覆盖率并不能像java那块那么简单,有专门的javascript的解析器,能够获取到这个js文件中所有的方法。所以套用原有的java那套逻辑基本是不太可行的。所以我们需要另辟蹊径来解决这个问题。

    java的增量代码diff 我们是从解析源码的文件入手的,那针对js既然这套不行,有没有方式能够从覆盖率结果数据入手,去解决这个事情呢?

    ## 思路
    那么是不是可以这么去考虑呢?从git diff中对比得到对应文件的改动行数,然后再对应到这块的数据上,如果修改的代码行 是在statemanMap/ fnMap/ branchMap 的覆盖范围的话就保留这块的数据,如果说改动行中,不存在有这块的内容则从对象中将这块的内容剔除掉。这样子就可以得到增量的数据了。

    但是我们是不是直接针对用户提交的coverage数据做处理呢?

    答案是不行的。 我们需要了解一个问题,之前我们在 聊聊前端代码覆盖率 (长文慎入) 中提到过用户提交的coverage数据并不是完整的反应到原本的代码行上,主要是针对typescript这块,因为如果你的编译是经过ts-loader -> babel-loader 处理的话。得到的coverage中的数据中的line的值,其实跟源码中的line会出现不一致的情况。而istanbul这块是会根据sourceMap 重新映射回去的。

    那哪里的数据才是正在正确的呢? 答案其实在通过nyc api生成的报告目录下, 当你的api指定了reporter包含有 json的情况下,就会在覆盖率报告的目录下生成有 coverage-final.json。 这里的数据其实跟coverage数据基本是一致的,并且这里的数据已经经过istanbul校正过。所以我们可以信任这块的数据。

    $_PS: 还在之前的逻辑(聊聊前端代码覆盖率 (长文慎入))里,包括浏览器提交的coverage,istanbul。大体思路是有的。
    –>

    计算增量测试覆盖率

    # 71 如何计算增量测试覆盖率
    发布于 2020-07-12 20:54

    计算增量测试覆盖率,总共需要3步:
    1) 计算出增量代码的所有行号
    2) 计算出测试未覆盖的代码的所有行号
    3) 对比计算增量代码被测试覆盖的比例,得出增量覆盖率

    一、计算增量代码的所有行号

    代码管理一般都会用到 GIT 这个工具,GIT提供了非常强大的管理增量代码的能力,因此,可以利用GIT这一特性,通过git diff(参考文献1) 这个命令获取增量代码。

    使用 git diff --unified=0 mastergit diff -U0 master看运行结果

    数据结构与不带options的结果基本一致,只不过第2部分和第3部分作为一个整体可能会出现1次或多次,还有一点变化是第2部分行号信息的表达出现了三种格式。
    git diff -U0 master | grep -Po '^\+\+\+ ./\K.*|^@@ -[0-9]+(,[0-9]+)? \+\K[0-9]+(,[0-9]+)?(?= @@)'

    python

    diff-cover: https://github.com/Bachmann1234/diff-cover

    Automatically find diff lines that need test coverage. Also finds diff lines that have violations (according to tools such as pycodestyle, pyflakes, flake8, or pylint).

    The diff-cover command line tool compares an XML coverage report with the output of git diff. It then reports coverage information for lines in the diff.

    增量质量

    diff内容是否都测试到了?

    # 可选
    Jest 原理/码研究
    jest 和 karma 覆盖率窥探:如何使用
    istanbul 覆盖率计算逻辑, 包括源码映射(sourceMap)

    # locv

    locv

    lcov geninfo: http://ltp.sourceforge.net/coverage/lcov/readme.php

    LCOV is an extension of GCOV, a GNU tool which provides information about
    what parts of a program are actually executed (i.e. “covered”) while running
    a particular test case.
    <!– The extension consists of a set of Perl scripts which build on the textual GCOV output to implement the following enhanced functionality:

    • HTML based output: coverage rates are additionally indicated using bar graphs and specific colors.
    • Support for large projects: overview pages allow quick browsing of coverage data by providing three levels of detail: directory view, file view and source code view.
      –>
knowledge is no pay,reward is kindness
0%